package ru.hh.team02.chat.client;

import ru.hh.team02.chat.utils.HashMapWithLock;
import ru.hh.team02.chat.utils.HashSetWithLock;

import java.util.*;

/**
 * (hopefully) thread-safe class which stores all clients.
 */
public class ClientRegistry {
    private final Map<String, HashSetWithLock<Client>> allClientsByRoom = new HashMap<String, HashSetWithLock<Client>>();
    private final HashMapWithLock<String, ClientData> clientDataByClientName = new HashMapWithLock<String, ClientData>();

    public ClientRegistry(String[] roomNames) {
        for (String name : roomNames) {
            addNewRoom(name);
        }
    }

    /**
     * Class to hold client data to get it by the client name through
     * clientDataByClientName Map.
     * 
     */
    class ClientData {
        private String room;
        private Client client;

        public ClientData(String room, Client client) {
            this.room = room;
            this.client = client;
        }

        public String getRoom() {
            return room;
        }

        public void setRoom(String room) {
            this.room = room;
        }

        public Client getClient() {
            return client;
        }

        public void setClient(Client client) {
            this.client = client;
        }
    }

    /**
     * Add a new chat room
     * 
     * @param name
     *            Name of the room
     * @return <code>true</code> if the room has been added; <code>false</code>
     *         if the room already exists
     */
    private boolean addNewRoom(String name) {
        if (allClientsByRoom.containsKey(name)) {
            return false;
        } else {
            allClientsByRoom.put(name, new HashSetWithLock<Client>());
            return true;
        }
    }

    /**
     * Get an unmodifiable view on all existing rooms
     * 
     * @return
     */
    public Set<String> getRoomNames() {
        // Please, note, that now the set of rooms is fixed after they are
        // loaded after starting service.
        // So, we don't need to use readLock to access allClientsByRoom and
        // neither we need to make a copy of it.
        // But when the set of room get modifiable, this method will need
        // to address this new functionality.
        return Collections.unmodifiableSet(allClientsByRoom.keySet());
    }

    /**
     * Get a shallow copy of the set with clients containing in the room. This
     * is expected to be the most often used method, so readLock will accept
     * simultaneous access to the data when no writing is performed (which will
     * be much less frequent and will only happen on client entering chat,
     * exiting and changing rooms).
     * 
     * @param name
     *            Room name to take the clients from
     * @return Set of clients.
     */
    @SuppressWarnings("unchecked")
    public Set<Client> getClientsFromRoom(String name) {
        if (!allClientsByRoom.containsKey(name)) {
            // no such room
            return null;
        }
        HashSetWithLock<Client> clients = allClientsByRoom.get(name);
        try {
            clients.getLock().readLock().lock();
            return (Set<Client>) clients.clone();
        } finally {
            clients.getLock().readLock().unlock();
        }

    }

    /**
     * Add client and register it.
     * 
     * @param client
     *            A client to be registered
     * @param room
     *            Room the client will be in.
     * @return <code>true</code> if client has been successfully registered;
     *         <code>false</code> if client hasn't been registered due to
     *         existing a client with the same name or not having the room with
     *         the name provided.
     */
    public boolean addClient(Client client, String room) {

        // Preliminary check. Just for optimization as requires only readLock
        if (hasClientWithName(client.getName())) {
            return false;
        }

        HashSetWithLock<Client> clientsFromRoom = allClientsByRoom.get(room);

        // Order of lock application must be preserved in all methods,
        // otherwise we may have a deadlock
        clientDataByClientName.getLock().writeLock().lock();
        clientsFromRoom.getLock().writeLock().lock();
        try {

            // Checking if the name hasn't appeared since preliminary check
            if (clientDataByClientName.containsKey(client.getName())) {
                return false;
            }

            clientDataByClientName.put(client.getName(), new ClientData(room,
                    client));
            clientsFromRoom.add(client);
        } finally {
            clientsFromRoom.getLock().writeLock().unlock();
            clientDataByClientName.getLock().writeLock().unlock();
        }
        return true;
    }

    /**
     * Unregister the client.
     * 
     * @param client
     *            A client to unregister.
     * @return <code>true</code> if client has been successfully unregistered;
     *         <code>false</code> if client is not among clients or haven't been
     *         found inside their room.
     */
    public boolean deleteClient(Client client) {
        try {
            // Order of lock application must be preserved in all methods,
            // otherwise we may have a deadlock
            clientDataByClientName.getLock().writeLock().lock();

            if (!clientDataByClientName.containsKey(client.getName())) {
                return false;
            }
            String room = clientDataByClientName.get(client.getName())
                    .getRoom();
            HashSetWithLock<Client> clientsFromRoom = allClientsByRoom
                    .get(room);

            try {
                clientsFromRoom.getLock().writeLock().lock();

                if (!clientsFromRoom.remove(client)) {
                    return false;
                }

                assert (clientDataByClientName.containsKey(client.getName()));

                clientDataByClientName.remove(client.getName());

            } finally {
                clientsFromRoom.getLock().writeLock().unlock();
            }
        } finally {
            clientDataByClientName.getLock().writeLock().unlock();
        }
        return true;
    }

    /**
     * Change room of the client.
     * 
     * @param client
     *            Client to change room for.
     * @param oldRoom
     *            The name of the room the client is currently in.
     * @param newRoom
     *            The name of the room to change to.
     * @return <code>true</code> if the room has been successfully changed;
     *         <code>false</code> if the newRoom with the name provided doesn't
     *         exist or if client is not in the oldRoom.
     */
    public boolean changeRoom(Client client, String newRoom) {
        if (!hasRoom(newRoom)) {
            return false;
        }

        HashSetWithLock<Client> clientsFromOldRoom = allClientsByRoom
                .get(getClientRoom(client.getName()));
        HashSetWithLock<Client> clientsFromNewRoom = allClientsByRoom
                .get(newRoom);
        try {
            // Order of lock application must be preserved in all methods,
            // otherwise we may have a deadlock
            clientsFromOldRoom.getLock().writeLock().lock();
            clientsFromNewRoom.getLock().writeLock().lock();
            clientDataByClientName.getLock().writeLock().lock();

            clientsFromOldRoom.remove(client);
            clientsFromNewRoom.add(client);
            clientDataByClientName.get(client.getName()).setRoom(newRoom);

        } finally {
            clientDataByClientName.getLock().writeLock().unlock();
            clientsFromNewRoom.getLock().writeLock().unlock();
            clientsFromOldRoom.getLock().writeLock().unlock();
        }
        return true;
    }

    public boolean hasRoom(String room) {
        return allClientsByRoom.containsKey(room);
    }

    public boolean hasClientWithName(String name) {
        try {
            clientDataByClientName.getLock().readLock().lock();
            return clientDataByClientName.containsKey(name);
        } finally {
            clientDataByClientName.getLock().readLock().unlock();
        }
    }

    public String getClientRoom(String name) {
        try {
            clientDataByClientName.getLock().readLock().lock();
            return clientDataByClientName.get(name).getRoom();
        } finally {
            clientDataByClientName.getLock().readLock().unlock();
        }
    }

    public Client getClientByName(String name) {
        try {
            clientDataByClientName.getLock().readLock().lock();
            return clientDataByClientName.get(name).getClient();
        } finally {
            clientDataByClientName.getLock().readLock().unlock();
        }
    }
}
